import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
import { Mesh } from '@babylonjs/core/Meshes/mesh';
import { Helper } from 'babylon-lib/tool/helper';
import { Scene } from '@babylonjs/core/scene';
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { PickingInfo } from '@babylonjs/core/Collisions/pickingInfo';
import { Ray } from '@babylonjs/core/Culling/ray';


interface ICameraAvoidBlockOptions {
    refRadius?: number;
    fixed?: boolean;
}


class CameraAvoidBlock {

    private _camera: ArcRotateCamera;
    private _targets: Mesh[] = [];
    private _enabled: boolean = false;
    private _scene: Scene;
    private _toPosition = new Vector3();
    private _fromPosition = new Vector3();
    private _ray = new Ray(new Vector3(0, 0, 0), new Vector3(0, 1, 0));
    private _result: PickingInfo[] = [];
    private _options: ICameraAvoidBlockOptions;
    private _offset: number = 0;

    constructor(camera: ArcRotateCamera, options?: ICameraAvoidBlockOptions) {
        this._camera = camera;
        this._scene = camera.getScene();
        this._options = options || {};
        this._options.refRadius = this._options.refRadius || 5;
        this._options.fixed = !!this._options.fixed;
    }

    public set fixed(fixed: boolean) {
        this._options.fixed = fixed;
    }

    public set refRadius(radius: number) {
        this._options.refRadius = radius || 5;
    }

    public set active(isEnable: boolean) {
        this._enabled = isEnable;
        isEnable ? this.attachEvents() : this.detachEvents();
    }

    public get active(): boolean {
        return this._enabled;
    }

    public addTargets(...targets: TransformNode[]): void {
        for (const target of targets) {
            Helper.getChildMeshes(target, false).forEach(m => {
                let est = false;
                for (const t of this._targets) {
                    if (t.uniqueId === m.uniqueId) {
                        est = true;
                        break;
                    }
                }
                if (!est) this._targets.push(m as Mesh);
            });
        }
    }

    public removeTargets(...targets: TransformNode[]): void {
        for (const target of targets) {
            Helper.getChildMeshes(target).forEach(m => {
                for (let i = 0; i < this._targets.length; i++) {
                    if (this._targets[i].uniqueId !== m.uniqueId) continue;
                    this._targets.splice(i, 1);
                    break;
                }
            });
        }
    }

    public clearTargets(): void {
        this._targets.length = 0;
    }

    private attachEvents(): void {
        this._scene.onBeforeRenderObservable.add(this.updateCamera.bind(this));
    }

    private detachEvents(): void {
        this._scene.onBeforeRenderObservable.removeCallback(this.updateCamera.bind(this));
    }

    private zoomCheck(): void {
        if (this._camera.inertialRadiusOffset == 0) return;
        this.refRadius = this._camera.radius;
    }

    private updateCamera(): void {
        this.zoomCheck();
        const fromP = this._fromPosition.copyFrom(this._camera.target);
        const toP = this._toPosition.copyFrom(this._camera.globalPosition);
        const ray = Ray.CreateFromToToRef(fromP, toP, this._ray);
        this._result.length = 0;
        const inc = ray.intersectsMeshes(this._targets, false, this._result);
        const r = this._camera.radius;
        const rr = this._options.refRadius!;
        if (!inc.length) {
            if (this._options.fixed) {
                if (r < rr) {
                    this._offset += 0.001;
                    this._camera.radius += (rr - r) * this._offset;
                }
            }
            return;
        }
        const dis = inc[0].distance;
        this._camera.radius = dis;
        this._offset = 0;
    }

}


export { CameraAvoidBlock };